--[[
MIT License

Copyright (c) 2023 Anggara Yama Putra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
-- Globals
AnggaraNothing = AnggaraNothing or {}
do
end

-- module: Base85
-- Ported from CPython's base64.py
-- https://github.com/python/cpython/blob/d828b35785eeb590b8ca92684038f33177989e46/Lib/base64.py
do
  local root = {}

  local strbyte = string.byte
  local strchar = string.char
  local strformat = string.format
  local tablepack = table.pack
  local tableunpack = table.unpack

  local function create_iter (len, step, ...)
    step = step or 1
    local args
    local unpack = tableunpack
    if select('#', ...) == 1 then
      local first = ...
      local first_type = type( first )
      if first_type == 'table' then
        args, len = first, len or #first
      elseif first_type == 'string' then
        args, len = first, len or #first
        unpack = strbyte
      end
    end
    args = args or tablepack( ... )
    len = len or args.n
    local pos, nextpos = 1, 1
    return function ()
      if nextpos > len then
        return
      end
      pos, nextpos = nextpos, nextpos + step
      return unpack(args, pos, nextpos-1)
    end, len
  end

  local function bytes_to_big_uint32 (...)
    local b1, b2, b3, b4 = ...
    if not b1 then
      return
    end
    b1, b2, b3, b4 = b1, b2 or 0, b3 or 0, b4 or 0
    return (
        (b1 << 24)
        + (b2 << 16)
        + (b3 << 8)
        + b4
    ) % 0x100000000
  end

  local function big_uint32_to_bytes (num)
    if not num then
      return
    end
    return (num >> 24) & 0xFF,
      (num >> 16) & 0xFF,
      (num >> 8) & 0xFF,
      num & 0xFF
  end

  -- 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
  -- abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
  local CHARSMAP = {
    [0]=48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74,
    75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100,
    101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
    121, 122, 33, 35, 36, 37, 38, 40, 41, 42, 43, 45, 59, 60, 61, 62, 63, 64, 94, 95,
    96, 123, 124, 125, 126
  }
  local CHARSMAP_IDX = {}
  do
    for i = 0, #CHARSMAP do
      CHARSMAP_IDX[CHARSMAP[i]] = i
    end
  end
  local PAD_CHAR = CHARSMAP[#CHARSMAP]
  local MAX_BUFFER_SIZE = 8192

  local encode
  do
    local function to_b85 (v)
      return CHARSMAP[v // 52200625],
      CHARSMAP[(v % 52200625) // 614125],
      CHARSMAP[(v % 614125) // 7225],
      CHARSMAP[(v % 7225) // 85],
      CHARSMAP[v % 85]
    end

    local echunks = {}
    --[[
    Encode args in base85 format and return a string.

    If pad is true, the input is padded with null so its length is a multiple of
    4 bytes before encoding.
    --]]
    encode = function (pad, ...)
      pad = pad or false
      local iter, len = create_iter( nil, 4, ... )
      local pos = 1
      for b1,b2,b3,b4 in iter do
        echunks[pos], echunks[pos+1],
        echunks[pos+2], echunks[pos+3],
        echunks[pos+4] = to_b85( bytes_to_big_uint32(b1,b2,b3,b4) )
        pos = pos + 5
      end
      pos = pos - 1
      local padding = (-len) % 4
      if not pad and padding > 0 then
        pos = pos - padding
      end

      local result = echunks
      if #echunks > MAX_BUFFER_SIZE then
        echunks = {}
      end
      return strchar(tableunpack(result, 1, pos))
    end
  end

  local decode
  do
    local badchar_err = 'bad base85 character at position %d'
    local overflow_err = 'base85 overflow in hunk starting at byte %d'
    local function from_b85 (pos, ...)
      local b1, b2, b3, b4, b5 = ...
      if not b1 then
        return
      end
      b1, b2, b3, b4, b5 = b1, b2 or PAD_CHAR, b3 or PAD_CHAR, b4 or PAD_CHAR, b5 or PAD_CHAR
      local result = 0

      local char_id = CHARSMAP_IDX[b1]
      if not char_id then
        error(strformat(
          badchar_err,
          pos
        ), 1)
      end
      result = result * 85 + char_id
      if result > 0xFFFFFFFF then
        error(strformat(
          overflow_err,
          pos
        ), 1)
      end
      char_id = CHARSMAP_IDX[b2]
      if not char_id then
        error(strformat(
          badchar_err,
          pos + 1
        ), 1)
      end
      result = result * 85 + char_id
      if result > 0xFFFFFFFF then
        error(strformat(
          overflow_err,
          pos
        ), 1)
      end
      char_id = CHARSMAP_IDX[b3]
      if not char_id then
        error(strformat(
          badchar_err,
          pos + 2
        ), 1)
      end
      result = result * 85 + char_id
      if result > 0xFFFFFFFF then
        error(strformat(
          overflow_err,
          pos
        ), 1)
      end
      char_id = CHARSMAP_IDX[b4]
      if not char_id then
        error(strformat(
          badchar_err,
          pos + 3
        ), 1)
      end
      result = result * 85 + char_id
      if result > 0xFFFFFFFF then
        error(strformat(
          overflow_err,
          pos
        ), 1)
      end
      char_id = CHARSMAP_IDX[b5]
      if not char_id then
        error(strformat(
          badchar_err,
          pos + 4
        ), 1)
      end
      result = result * 85 + char_id
      if result > 0xFFFFFFFF then
        error(strformat(
          overflow_err,
          pos
        ), 1)
      end
      return big_uint32_to_bytes(result)
    end

    local dchunks = {}
    --[[
    Decode base85-encoded bytes-like object or string `b`

    The result is returned as arguments.
    --]]
    decode = function (...)
      local iter, len = create_iter( nil, 5, ... )
      local i = 1
      for b1,b2,b3,b4,b5 in iter do
        dchunks[i], dchunks[i+1],
        dchunks[i+2], dchunks[i+3] = from_b85( i, b1,b2,b3,b4,b5 )
        i = i + 4
      end
      i = i - 1
      local padding = (-len) % 5
      if padding > 0 then
        i = i - padding
      end

      local result = dchunks
      if #dchunks > MAX_BUFFER_SIZE then
        dchunks = {}
      end
      return tableunpack(result, 1, i)
    end
  end

  local function decode_as_string (...)
    return strchar(decode(...))
  end

  root.encode = encode
  root.decode = decode
  root.decode_as_string = decode_as_string
  AnggaraNothing.Base85 = root

  --[[ tests
  do
    local samples = {
      {"hello world!", 'Xk~0{Zy<MXa%^NF',},
      {"hello world!!", 'Xk~0{Zy<MXa%^NFAp', 'Xk~0{Zy<MXa%^NFApigX'},
      {"мир привет!", '(7e#N(SRV(ztMouxX`lDwb6nh',},
      {"мир привет!!", '(7e#N(SRV(ztMouxX`lDwb6nhAp','(7e#N(SRV(ztMouxX`lDwb6nhApigX'},
    }

    local tableconcat = table.concat
    local tablemove = table.move
    local debug = log or print
    local rencode, rdecode, rdecode_as_string = root.encode, root.decode, root.decode_as_string

    for _,v in ipairs(samples) do
      local textstr, trimstr, padstr = tableunpack( v )
      padstr = padstr or trimstr
      local textbytes = tablepack( strbyte(textstr, 1, -1) )
      local textcc = tableconcat( textbytes, ',' )

      debug( '---String---' )
      local encodedstr = rencode( nil, textstr )
      debug(strformat( '"%s" : "%s"', textstr, encodedstr ))
      assert( encodedstr == trimstr, '[str] encoder failed' )

      encodedstr = rencode( true, textstr )
      debug(strformat( '"%s" : "%s"', textstr, encodedstr ))
      assert( encodedstr == padstr, '[str] padding encoder failed' )

      local decodedstr = rdecode_as_string( trimstr )
      debug(strformat( '"%s" : "%s"', textstr, decodedstr ))
      assert( decodedstr == textstr, '[str] decoder failed' )

      decodedstr = rdecode_as_string( padstr )
      decodedstr = decodedstr:sub( 1, #textstr )
      debug(strformat( '"%s" : "%s"', textstr, decodedstr ))
      assert( decodedstr == textstr, '[str] padding decoder failed' )

      decodedstr = rdecode_as_string( rencode(nil, textstr) )
      debug(strformat( '"%s" : "%s"', decodedstr, textstr ))
      assert( decodedstr == textstr, '[str] roundtrip failed' )

      decodedstr = rdecode_as_string( rencode(true, textstr) )
      decodedstr = decodedstr:sub( 1, #textstr )
      debug(strformat( '"%s" : "%s"', decodedstr, textstr ))
      assert( decodedstr == textstr, '[str] padding roundtrip failed' )

      debug( '---Bytes Table---' )
      encodedstr = rencode( nil, textbytes )
      debug(strformat( '%s : "%s"', textcc, encodedstr ))
      assert( encodedstr == trimstr, '[tb] encoder failed' )

      encodedstr = rencode( true, textbytes )
      debug(strformat( '%s : "%s"', textcc, encodedstr ))
      assert( encodedstr == padstr, '[tb] padding encoder failed' )

      local decodedbytes = tablepack(rdecode( trimstr ))
      local decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : "%s"', decodedcc, textstr ))
      assert( decodedcc == textcc, '[tb] decoder failed' )

      decodedbytes = tablepack(rdecode( padstr ))
      decodedbytes = tablemove( decodedbytes, 1, #textbytes, 1, {} )
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : "%s"', decodedcc, textstr ))
      assert( decodedcc == textcc, '[tb] padding decoder failed' )

      decodedbytes = tablepack(rdecode( rencode(nil, textbytes) ))
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : %s', textcc, decodedcc ))
      assert( decodedcc == textcc, '[tb] roundtrip failed' )

      decodedbytes = tablepack(rdecode( rencode(true, textbytes) ))
      decodedbytes = tablemove( decodedbytes, 1, #textbytes, 1, {} )
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : %s', textcc, decodedcc ))
      assert( decodedcc == textcc, '[tb] padding roundtrip failed' )

      debug( '---Varags---' )
      local trimbytes = tablepack( strbyte(trimstr, 1, -1) )
      local padbytes = tablepack( strbyte(padstr, 1, -1) )

      encodedstr = rencode( nil, tableunpack(textbytes) )
      debug(strformat( '%s : "%s"', textcc, encodedstr ))
      assert( encodedstr == trimstr, '[va] encoder failed' )

      encodedstr = rencode( true, tableunpack(textbytes) )
      debug(strformat( '%s : "%s"', textcc, encodedstr ))
      assert( encodedstr == padstr, '[va] padding encoder failed' )

      decodedbytes = tablepack(rdecode( tableunpack(trimbytes) ))
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : "%s"', decodedcc, textstr ))
      assert( decodedcc == textcc, '[va] decoder failed' )

      decodedbytes = tablepack(rdecode( tableunpack(padbytes) ))
      decodedbytes = tablemove( decodedbytes, 1, #textbytes, 1, {} )
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : "%s"', decodedcc, textstr ))
      assert( decodedcc == textcc, '[va] padding decoder failed' )

      decodedbytes = tablepack(rdecode( rencode(nil, tableunpack(textbytes)) ))
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : %s', textcc, decodedcc ))
      assert( decodedcc == textcc, '[va] roundtrip failed' )

      decodedbytes = tablepack(rdecode( rencode(true, tableunpack(textbytes)) ))
      decodedbytes = tablemove( decodedbytes, 1, #textbytes, 1, {} )
      decodedcc = tableconcat( decodedbytes, ',' )
      debug(strformat( '%s : %s', textcc, decodedcc ))
      assert( decodedcc == textcc, '[va] padding roundtrip failed' )
    end
  end
  --]]
end
